#Writing graphs by hand is not very easy. 
#Thus, this class may be messy.
#Important parts:
# - reads data to display from two hashes, data and data2. They are both keyed with Date/Times. 
#   eg { "Fri May 23 17:39:53 +1000 2008" => 12 }
# - draws this data!
# - Redraws the graph when it is resized, or explicitly via 'redraw()'
# - the graph gtk widget is accessible through 'graph()'
#
# * All of the methods that draw things(data plots, axis, etc) use four functions, 
#  y_pt(), x_pt(), x_delta and y_delta. These abstract away the scaling, margins etc so each data/axis set
#  can just plot values as is without translating them into pixel co-ords. 

class Graph
	attr_accessor :data, :data2
	attr_reader :graph

  #can set most of the 'look n feel' of the graph through an options hash. 
  #for any option not present, we use a sensible default.
	def initialize(data1 = nil, data2 = nil, options = {})
		
		#if we've got no data, display some random data for testing.
		@data = data1 || (1..20).inject({}){|hash,n| hash.merge({Time.now + n => rand(20)}) }
		@data2 = data2 
			
		@title = options[:title] || 'no title'
		@x_size = options[:x_size] || 300
		@y_size = options[:y_size] || 200
		@colour1 = options[:colour1] || 'red'
		@colour2 = options[:colour2] || 'royalblue'
		@left_margin = 50
		@bottom_margin = 50
		@top_margin = 20
		@right_margin = 50
		
		#granularities control how many data values for every axis marker. 
		# Eg 2 would mean for every 2nd x value show a notch.
		@y_granularity = 2
		@x_granularity = 1
		@x_label_granularity = 10  #how many values b/w labels
		@x_label_minimum_pixel_width = 10  #minimum pixel size to keep for a label on x_axis
		@left_legend = options[:left_legend] || "C"
		@right_legend = options[:right_legend] || "mm"
		@font_pt = 10
		@graph =  Gtk::DrawingArea.new
		draw
	end
	
	#force a redraw of the graph, thus a resize, update of data etc.
	def redraw
		@graph.queue_resize
		@graph.queue_draw
	end
	
	#Most of the magic happens inside the 'expose_event', inside here.
	def draw
		#@graph.clear
		@graph.set_size_request(@x_size,@y_size)
		@graph.signal_connect("expose_event") do

			ctx = @graph.window.create_cairo_context
			
			#draw the title text of the graph. Its a surprising amount of work.
			p = ctx.create_pango_layout
			p.font_description = Pango::FontDescription.new("Times #{@font_pt}")
			gc = Gdk::GC.new(@graph.window)
			p.text = @title
			@graph.window.draw_layout(gc,@graph.allocation.width/2,0,p)

			#scale the graph data/axis... 
			recalculate_x_axis(ctx) 
			#recalculate_y_axis(ctx)
			
			#then draw it!
			#calling calc_[whatever]_delta allows the scaling of the axis to different data sets
			draw_y_axis_left(ctx, calc_y_delta(@data)   )
			draw_y_axis_right(ctx, calc_y_delta(@data2) ) if @data2
			draw_x_axis(ctx)
			graph_data(ctx,@data,@colour1) 
			graph_data(ctx,@data2,@colour2) if @data2
	
		  true  
		end

		return @graph	
	end
	
	#this method is brilliant. Recalculate the granuality of x_labels on the fly, to prevent them overlapping. 
	def recalculate_x_axis(ctx)
		working_pixel_width = @graph.allocation.width - @left_margin - @right_margin
		max_num_labels = working_pixel_width / (@x_label_minimum_pixel_width * 2)
		@x_label_granularity = @data.keys.length / max_num_labels     #get the num of datums per label
		@x_label_granularity = 1 if @x_label_granularity < 1
	end

	#def recalculate_y_axis(ctx)
	#	working_pixel_width = @graph.allocation.width - @left_margin - @right_margin
	#	max_num_labels = working_pixel_width / (@y_label_minimum_pixel_width * 2)
	#	@y_label_granularity = @data.values.max / max_num_labels     #get the num of datums per label
	#	@y_label_granularity = 1 if @y_label_granularity < 1
#	end
	
	#draw the actual data points. 
	def graph_data(ctx,data,color)
		y_delta = calc_y_delta(data)
		x = 0
		data.keys.sort.each do |key|
			ctx.line_to( xpt(x), ypt(data[key] * y_delta) )
			x += x_delta
		end
			
		ctx.set_source_color( Gdk::Color.parse( color ) )
		ctx.set_line_width( 1.0 )
		ctx.stroke
		ctx.fill  
	end

	def draw_y_axis_left(ctx, y_delta)
		#its like that LOGO turtle all over again. PU  FORWARD 50 PD  ARGH!
		ctx.move_to( xpt,ypt)  #move to origin
		ctx.line_to( xpt, y_max)#ypt(@data.values.max * y_delta) )  #draw y axis
		ctx.move_to( xpt, ypt )
		ctx.line_to( xpt(@data.length * x_delta), ypt) #draw x axis
		
		gc = Gdk::GC.new(@graph.window)
		fd = Pango::FontDescription.new("Times #{@font_pt} ")
		for mark in y_marks
		  #next if mark == 0
			ctx.move_to xpt, ypt(mark * y_delta)
			ctx.line_to xpt(-5), ypt(mark * y_delta)
			
			p = ctx.create_pango_layout
			p.text = mark.to_s
			p.font_description = fd
			size_x,size_y = p.pixel_size
			p "xpt#{xpt} - #{size_x} - 10, ypt(#{mark} * #{y_delta}) - (#{size_y}/2)"
			@graph.window.draw_layout(gc, xpt - size_x - 10, ypt(mark * y_delta) - (size_y/2),p)
		end
		p = ctx.create_pango_layout
		p.text = @left_legend 
		@graph.window.draw_layout(gc,5,@graph.allocation.height/2,p)
		
		ctx.set_source_color( Gdk::Color.parse( "black" ) )
		ctx.set_line_width( 1.0 )
		ctx.stroke
	end
	
	#draw a y axis on the RHS
	def draw_y_axis_right(ctx,y_delta)
		#its like that fucking LOGO turtle all over again. PU  FORWARD 50 PD  ARGH!
		
		ctx.move_to( xpt(0,true),ypt)  #move to origin
		ctx.line_to( xpt(0,true), y_max)
		ctx.move_to( xpt(0,true), ypt )
		ctx.line_to( xpt(@data.length * x_delta, true), ypt) #draw x axis
		
		gc = Gdk::GC.new(@graph.window)
		fd = Pango::FontDescription.new("Times #{@font_pt} ")
		for mark in y_marks
		  #next if mark == 0
			ctx.move_to xpt(0,true), ypt(mark * y_delta)
			ctx.line_to xpt(-5,true), ypt(mark * y_delta)
			
			p = ctx.create_pango_layout
			p.text = mark.to_s
			p.font_description = fd
			size_x,size_y = p.pixel_size
		#	p "xpt#{xpt} - #{size_x} - 10, ypt(#{mark} * #{y_delta}) - (#{size_y}/2)"
			@graph.window.draw_layout(gc, xpt(0,true) + 10 , ypt(mark * y_delta) - (size_y/2),p)
		end
		p = ctx.create_pango_layout
		p.font_description = fd
		p.text = @right_legend 
		@graph.window.draw_layout(gc,xpt(-25,true),@graph.allocation.height/2,p)
		
		ctx.set_source_color( Gdk::Color.parse( "black" ) )
		ctx.set_line_width( 1.0 )
		ctx.stroke
	end
	
	
	def draw_x_axis(ctx)
		gc = Gdk::GC.new(@graph.window)
		fd = Pango::FontDescription.new("Times #{@font_pt} ")
		for mark in x_marks
			ctx.move_to xpt(mark * x_delta), ypt
			ctx.line_to xpt(mark * x_delta), ypt(-5)
		end
		@data.keys.sort.each_with_index do |key,i|	
			next if i % @x_label_granularity != 0
			p = ctx.create_pango_layout
			p.text = key.strftime("%d/%m/%y\n%H:%M:%S")
			p.alignment = Pango::ALIGN_CENTER
			p.font_description = fd
			size_x,size_y = p.pixel_size
			@x_label_minimum_pixel_width = size_x
			@graph.window.draw_layout(gc, xpt(i * x_delta) - (size_x/2), ypt + size_y/2,p)
			ctx.move_to xpt(i * x_delta), ypt
			ctx.line_to xpt(i * x_delta), ypt(-10)
		end
		
		ctx.set_source_color( Gdk::Color.parse( "black" ) )
		ctx.set_line_width( 1.0 )
		ctx.stroke
	end
	
	#returns how many pixels per 'unit' of the x data set.
	def x_delta
		alloc = @graph.allocation
		return (alloc.width - @left_margin - @right_margin) / @data.length.to_f
	end
	
	#returns how many pixels per 'unit' of y for the given data set.
	def calc_y_delta(data_set)
		graph_height = @graph.allocation.height - @bottom_margin - @top_margin
		if data_set.values.max == 0
		  return graph_height / @y_granularity
		else
		  return graph_height / (data_set.values.max.to_f +  @y_granularity )
		end
	end
	
	def y_max
		@top_margin
	end
	def x_max
		@graph.allocation.width - @right_margin
	end
	
	#translate a y_pt into pixel coords
	def ypt(y = 0)  
		#gtk drawable has (0,0) at the *top* left - no good, so we have to set points relative to the bottom left.
		@graph.allocation.height - @bottom_margin - y
	end
	
	#translate a x_pt into pixel coords
	def xpt(x = 0, reverse_axis = false)
		if reverse_axis
			x_max - x
		else
			@left_margin + x
		end
	end
	#get an array of numbers that desribes the positions we should put labels at along the axises 
	def y_marks
		(0..(@data.values.max / @y_granularity)+@y_granularity).collect{|y| y *  @y_granularity}
	end
	def x_marks
		(0..(@data.keys.length / @x_granularity)+1).collect{|x| x * @x_granularity}
	end
end
